package vlcjdemo;

import java.awt.event.MouseMotionListener;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Image;
import java.awt.LayoutManager;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import javax.swing.ActionMap;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JToolTip;
import javax.swing.JViewport;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

/**
 * This image list requests images in
 * a interface that can be asynchronous.
 *
 * All images have a in memory cache in this
 * class, but only a limited number will be on
 * at a time, so you should use a secondary image
 * disk cache. When this needs a image it requests
 * it in RenderValues.getCellImage and you should
 * return it in ImageList.returnImage. If you don't, 
 * it will never ask again. To request to try again
 * at a later time use ImageList.deferImage.
 *
 * All images are removed on removeNotify, and the not 
 * displayed ones are regularly removed on a variable 
 * number of requests relating to the screen
 * resolution (minimum) and requested again when needed.
 *
 * To ask if the list is at the right place to display
 * a Image for a Object use isObjectVisible() 
 * (for async loading), no need to call it on the EDT.
 * @author Owner
 */
public final class ImageList {

    private final JScrollPane layer = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    WrappedList list;
    private final RenderValues update;
    //Only use this cache on the EDT. Even if synchronized it would
    //throw concurrent modification exceptions because iterators are used.
    private final Map<Object, Image> memoryCache = new IdentityHashMap<Object, Image>();

    /**
     * Use this to make the image list retry the request
     * for the Image for this value at a later date if already
     * requested and its if not needed now (using isObjectInvisible).
     * @param valueAsKey
     * @throws IllegalArgumentException if valueAsKey is null.
     */
    public void deferImage(final Object valueAsKey) {
        if (valueAsKey == null) {
            throw new IllegalArgumentException("Can't return a null argument");
        }
        if (SwingUtilities.isEventDispatchThread()) {
            memoryCache.remove(valueAsKey);
        } else {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    memoryCache.remove(valueAsKey);
                }
            });
        }
    }

    /**
     * Use this to put a image into a cell of the list.
     * @param valueAsKey
     * @param image
     * @throws IllegalArgumentException if a argument is null.
     */
    public void putImage(final Object valueAsKey, final BufferedImage image) {
        if (image == null || valueAsKey == null) {
            throw new IllegalArgumentException("Can't return a null argument");
        }

        if (SwingUtilities.isEventDispatchThread()) {
            repaintForObject(valueAsKey, image);
            //see http://www.pushing-pixels.org/?p=369
        } else if (list.isVisible() && list.isShowing()) {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    repaintForObject(valueAsKey, image);
                }
            });
        }
    }

    private void repaintForObject(final Object valueAsKey, final Image image) {
        memoryCache.put(valueAsKey, image);
        if (innerIsObjectVisible(valueAsKey)) {
            layer.repaint();
        }
    }

    /**
     * @return the object at a given ImageList point, if any. Otherwise return null.
     */
    public Object locationToObject(Point location) {
        int index = list.locationToIndex(location);
        if (index >= 0) {
            return list.getModel().getElementAt(index);
        }

        return null;
    }

    /**
     * @return the objects that are currently selected
     * return a empty array if nothing is selected
     */
    public Object[] getSelectedObjects() {
        return list.getSelectedValues();
    }

    /**
     * {@link  javax.swing.JList#addMouseListener(MouseListener listener) view class method}
     */
    public void addMouseListener(MouseListener listener) {
        list.addMouseListener(listener);
    }

    public void addListSelectionListener(ListSelectionListener listener) {
        list.addListSelectionListener(listener);
    }

    public void addKeyListener(KeyListener listener) {
        list.addKeyListener(listener);
    }

    /**
     * {@link  javax.swing.JList#removeMouseListener(MouseListener listener) view class method}
     */
    public void removeMouseListener(MouseListener listener) {
        list.removeMouseListener(listener);
    }

    /**
     * {@link  javax.swing.JList#getInputMap() view class method}
     */
    public InputMap getInputMap() {
        return list.getInputMap();
    }

    /**
     * {@link  javax.swing.JList#getActionMap() view class method}
     */
    public ActionMap getActionMap() {
        return list.getActionMap();
    }

    /**
     * Returns if the list object is not visible in the viewport
     * @param obj
     * @return
     */
    public boolean isObjectInvisible(final Object obj) {
        if (EventQueue.isDispatchThread()) {
            return !innerIsObjectVisible(obj);
        }

        FutureTask<Boolean> result = new FutureTask<Boolean>(new Callable<Boolean>() {

            @Override
            public Boolean call() throws Exception {
                return !innerIsObjectVisible(obj);
            }
        });
        EventQueue.invokeLater(result);
        try {
            return result.get();
        } catch (Exception ex) {
            throw Tests.assertionError("Impossible!", ex);
        }
    }

    private boolean innerIsObjectVisible(Object obj) {
        int first = list.getFirstVisibleIndex();
        if (first > -1) {
            ListModel model = list.getModel();
            int last = list.getLastVisibleIndex();
            for (int i = first; i <= last; i++) {
                if (model.getElementAt(i).equals(obj)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * As images are a heavyweight object ImageList has a lazy
     * strategy to create and dispose them after use. To implement
     * this optimally, the getCellImage method can offload image
     * creation to another thread, if it needs to be loaded -
     * images are returned by calling ImageList.returnImage(Object value, Image).
     * If you don't call it, images are assumed not to exist for the index,
     * as if you call it with a null image.
     */
    public interface RenderValues {

        /**
         * Given a list object value and desired width and height give a
         * appropriate image. This method has to call returnImage(Object value, Image)
         * (can be called in another thread). If you offload image loading
         * to a thread call returnImage in the thread after loading.
         * If you don't want to offload image loading read the image and then call
         * returnImage directly.
         *
         * @param obj not null
         * @param imageWidth
         * @param imageHeight
         */
        void requestCellImage(ImageList list, Object obj, int imageWidth, int imageHeight);

        /**
         * Given a list object and desired width and height give a
         * appropriate cell text.
         * @param obj not null
         * @return cell text
         */
        String getCellText(ImageList list, Object obj);

        /**
         * Given a list object and desired cell give a
         * appropriate tooltip text.
         * @param obj not null
         * @param tooltipFont the tooltip FontMetrics
         * @return cell tooltip text
         */
        String getTooltipText(ImageList list, FontMetrics tooltipMetrics, Object obj);
    }

    /**
     * To display the image list. If you want
     * the scrollbar elsewhere use the getVerticalScrollBar()
     * method.
     */
    public JComponent getView() {
        return layer;
    }

    public void clean(int cellWidth, int cellHeight, Object[] listData) {
        list.removeAll();
        JViewport port = layer.getViewport();
        port.remove(list);
        init(new WrappedList(listData), cellWidth, cellHeight);
    }

    public void setItems(Object[] listData) {
        list.setListData(listData);
        list.repaint();
    }

    public ImageList(int cellWidth, int cellHeight, RenderValues update, Object[] listData) {
        super();
        this.update = update;
        init(new WrappedList(listData), cellWidth, cellHeight);
    }

    private void init(WrappedList list, int cellWidth, int cellHeight) {
        this.list = list;
        //for text (? assume 30) and borders (20*2)
        int imageHeight = Math.max(0, cellHeight - 24);
        int imageWidth = Math.max(0, cellWidth - 6);
        ImageFileListCellRenderer renderer = new ImageFileListCellRenderer(imageWidth, imageHeight);
        list.setCellRenderer(renderer);
        list.setFixedCellWidth(cellWidth);
        list.setFixedCellHeight(cellHeight);
        list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
        list.setVisibleRowCount(-1);
        list.setInheritsPopupMenu(true);
        layer.setInheritsPopupMenu(true);
        list.setOpaque(true);
        renderer.renderer.setOpaque(true);
        layer.setOpaque(true);
        JViewport port = layer.getViewport();
        port.setOpaque(true);
        port.setLayout(new CenteredViewPortLayout());
        port.add(list);
        //don't allow the laf to be a smart-ass
        //(sometimes they replace uiresource colors
        //acording to the components).
        //Setting a "normal" color avoid that in most of them
        Color viewColor = new Color(UIManager.getColor("List.background").getRGB());
        port.setBackground(viewColor);
        layer.setBackground(viewColor);
        list.setBackground(viewColor);
        renderer.renderer.setBackground(viewColor);
        renderer.renderer.setForeground(new Color(UIManager.getColor("List.foreground").getRGB()));
    }

    private final class WrappedList extends JList implements MouseWheelListener, MouseListener, MouseMotionListener, ListSelectionListener {

        private final int defaultTimeout = ToolTipManager.sharedInstance().getInitialDelay();
        private final JToolTip tooltip = new CellCenteredToolTip();
        private final FontMetrics tooltipMetrics = tooltip.getFontMetrics(tooltip.getFont());
        private boolean insideList = false;

        public WrappedList(Object[] listData) {
            super(listData);
            init();
        }

        public WrappedList(ListModel dataModel) {
            super(dataModel);
            init();
        }

        private void init() {
            //show tooltips immediatly & send events (cheat)
            addMouseListener(this);
            addMouseMotionListener(this);
            //show tooltips after scrolling & moving.
            layer.addMouseWheelListener(this);
            addListSelectionListener(this);
            //passthrough the wheelscroll event (to allow the scrollpane to move, as by default)
            tooltip.addMouseWheelListener(new MouseWheelListener() {

                @Override
                public void mouseWheelMoved(MouseWheelEvent e) {
                    layer.dispatchEvent(SwingUtilities.convertMouseEvent(tooltip, e, layer));
                }
            });
            //passthrough the mouse event to registered mouse listeners that matter (exposed on interface)
            tooltip.addMouseListener(new TooltipMouseDispatcher());
            tooltip.setBackground(Color.lightGray);
        }

        //filter the mouse events fromtooltipmanager, and only send the events we want to
        @Override
        public synchronized void addMouseListener(MouseListener l) {
            if (l != ToolTipManager.sharedInstance()) {
                super.addMouseListener(l);
            }
        }

        @Override
        public synchronized void addMouseMotionListener(MouseMotionListener l) {
            if (l != ToolTipManager.sharedInstance()) {
                super.addMouseMotionListener(l);
            }
        }

        @Override
        public synchronized void addMouseWheelListener(MouseWheelListener l) {
            if (l != ToolTipManager.sharedInstance()) {
                super.addMouseWheelListener(l);
            }
        }

        @Override
        public JToolTip createToolTip() {
            return tooltip;
        }

        @Override
        public String getToolTipText(MouseEvent event) {
            Point point = event.getPoint();
            int index = this.locationToIndex(point);
            //Get the value of the item in the list
            if (index == -1) {
                return null;
            }
            String s = update.getTooltipText(ImageList.this, tooltipMetrics, getModel().getElementAt(index));
            //assure this resizes? (bug in windows TODO check testcase)
            tooltip.setTipText(s);
            return s;
        }

        @Override
        public Point getToolTipLocation(MouseEvent event) {
            int mouseIndex = list.locationToIndex(event.getPoint());
            if (mouseIndex == -1) {
                return null;
            }
            ImageFileListCellRenderer cr = (ImageFileListCellRenderer) list.getCellRenderer();
            Rectangle r = getCellBounds(mouseIndex, mouseIndex);
            if (r == null) {
                return null;
            }
            int textStart = cr.renderer.getBaseline(r.width, r.height)
                    - getFontMetrics(getFont()).getMaxAscent();

            Point scrollPt = SwingUtilities.convertPoint(this, r.x, r.y + textStart, layer);
            if (scrollPt.y > layer.getHeight()) {
                //good ui's will limit this to the screen height,
                //no need to subtract the tooltip height on that special case.
                return SwingUtilities.convertPoint(layer, scrollPt.x, layer.getHeight(), this);
            }
            return new Point(r.x, r.y + textStart);
        }

        /**
         * Memory optimization
         */
        @Override
        public void removeNotify() {
            super.removeNotify();
            Iterator<Image> savedPictures = memoryCache.values().iterator();
            while (savedPictures.hasNext()) {
                Image picture = savedPictures.next();
                if (picture != null) {
                    savedPictures.remove();
                    picture.flush();
                }
            }
            if (insideList) {
                //remove any popup that is visible and restore state too.
                MouseEvent phantom = new MouseEvent(this, MouseEvent.MOUSE_EXITED, System.currentTimeMillis(), 0, 0, 0, 0, false);
                mouseExited(phantom);
            }
        }

        //Listener methods (remember to dispatch to tooltipmanager when needed)
        @Override
        public void mouseWheelMoved(final MouseWheelEvent e) {
            //component.getMousePosition always returns null 
            //in this component. I dont know why.
            Point mouseLocation = MouseInfo.getPointerInfo().getLocation();
            SwingUtilities.convertPointFromScreen(mouseLocation, this);

            if (mouseLocation == null) {
                return;
            }
            MouseEvent phantom = new MouseEvent(this, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, mouseLocation.x, mouseLocation.y, 0, false);
            ToolTipManager.sharedInstance().mouseMoved(phantom);
        }

        @Override
        public void valueChanged(final ListSelectionEvent e) {
            if (e.getValueIsAdjusting()) {
                return;
            }
            int index = getSelectedIndex();
            if (index == -1) {
                return;
            }
            Rectangle r = getCellBounds(index, index);
            if (r == null) {
                return;
            }
            Point p = r.getLocation();
            MouseEvent phantom = new MouseEvent(this, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, p.x, p.y, 0, false);
            ToolTipManager.sharedInstance().mouseMoved(phantom);
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            final ToolTipManager sharedInstance = ToolTipManager.sharedInstance();
            sharedInstance.setInitialDelay(0);
            sharedInstance.mouseEntered(e);
            insideList = true;
        }

        @Override
        public void mouseExited(MouseEvent e) {
            final ToolTipManager sharedInstance = ToolTipManager.sharedInstance();
            sharedInstance.mouseExited(e);
            sharedInstance.setInitialDelay(defaultTimeout);
            insideList = false;
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            //don't dispatch to tooltipmanager
        }

        @Override
        public void mousePressed(MouseEvent e) {
            //don't dispatch to tooltipmanager
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            //don't dispatch to tooltipmanager
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            //don't dispatch to tooltipmanager
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            ToolTipManager.sharedInstance().mouseMoved(e);
        }

        private final class CellCenteredToolTip extends JToolTip {

            public CellCenteredToolTip() {
                super();
//                Font font = WrappedList.this.getFont();
//                setFont(font.deriveFont(font.getSize() + 5F));
            }

            @Override
            public Dimension getPreferredSize() {
                Dimension ps = super.getPreferredSize();
                if (ps.width < getFixedCellWidth()) {
                    ps.width = getFixedCellWidth();
                }
                return ps;
            }
        }

        /**
         * What can should be translated is.
         * Maybe it needs some sanity checks (mouse event is inside)
         */
        private final class TooltipMouseDispatcher implements MouseListener {

            @Override
            public void mouseClicked(MouseEvent e) {
                list.dispatchEvent(SwingUtilities.convertMouseEvent(tooltip, e, WrappedList.this));
            }

            @Override
            public void mousePressed(MouseEvent e) {
                list.dispatchEvent(SwingUtilities.convertMouseEvent(tooltip, e, WrappedList.this));
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                list.dispatchEvent(SwingUtilities.convertMouseEvent(tooltip, e, WrappedList.this));
            }

            @Override
            public void mouseEntered(MouseEvent e) {
            }

            @Override
            public void mouseExited(MouseEvent e) {
            }
        }
    }

    private final class ImageFileListCellRenderer extends MouseAdapter implements ListCellRenderer {

        public final int MAX_IMAGES_REQUESTS;
        private final DefaultListCellRenderer renderer = new DefaultListCellRenderer();
        private final Icon emptyIcon;
        private final ImageIcon imgIcon = new ImageIcon();
        private final int width, height;
        private int counter;
        public Object mouseValue;
        public int mouseIndex = -1;
        public GlowFilter glow = new GlowFilter(0.04F);

        public ImageFileListCellRenderer(final int width, final int height) {
            super();
            this.width = width;
            this.height = height;
            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            MAX_IMAGES_REQUESTS = Math.max(30, (screenSize.width / width * screenSize.height / height) * 3);
            emptyIcon = new EmptyIcon(width, height);
            renderer.setHorizontalAlignment(SwingConstants.CENTER);
            renderer.setVerticalAlignment(SwingConstants.BOTTOM);
            renderer.setVerticalTextPosition(JLabel.BOTTOM);
            renderer.setHorizontalTextPosition(JLabel.CENTER);
            list.addMouseMotionListener(this);
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, final int index, boolean isSelected, boolean cellHasFocus) {
            BufferedImage img = (BufferedImage) memoryCache.get(value);
            //null was put there. No image in cache...yet
            if (img == null && memoryCache.containsKey(value)) {
                return asTextOnly(list, update.getCellText(ImageList.this, value), isSelected);
            }
            if (img == null) {
                //return null until image is not null
                memoryCache.put(value, null);
                //should call repaint later
                update.requestCellImage(ImageList.this, value, width, height);
                //optimization (dont wait for the repaint if the request is put without threading)
                img = (BufferedImage) memoryCache.get(value);
                if (img == null) {
                    return asTextOnly(list, update.getCellText(ImageList.this, value), isSelected);
                }
            }

            //gc the old images... once in a while
            if (counter++ == MAX_IMAGES_REQUESTS) {
                counter = 0;
                disposeInvisibleImages(list);
            }

            if (mouseValue == value) {
                img = glow.filter(img, null);
            }
            return asTextAndImage(list, update.getCellText(ImageList.this, value), img, isSelected);
        }

        private JLabel asTextOnly(JList list, String text, boolean focused) {
            renderer.getListCellRendererComponent(list, null, -1, focused, false);
            renderer.setIcon(emptyIcon);
            renderer.setText(text);
            return renderer;
        }

        private JLabel asTextAndImage(JList list, String text, BufferedImage img, boolean focused) {
            renderer.getListCellRendererComponent(list, null, -1, focused, false);
            imgIcon.setImage(img);
            renderer.setIcon(imgIcon);
            renderer.setText(text);
            return renderer;
        }

        private void disposeInvisibleImages(JList list) {
            int firstIndex = list.getFirstVisibleIndex();
            int lastIndex = list.getLastVisibleIndex();
            //A direct index mapping wouldn't work since listmodel can be mutable
            List tmp = new ArrayList();
            ListModel model = list.getModel();
            for (int i = firstIndex; i <= lastIndex; i++) {
                Object key = model.getElementAt(i);
                if (memoryCache.containsKey(key)) {
                    tmp.add(key);
                    tmp.add(memoryCache.remove(key));
                }
            }
            Iterator<Image> values = memoryCache.values().iterator();
            while (values.hasNext()) {
                Image disposable = values.next();
                if (disposable != null) {
                    values.remove();
                    disposable.flush();
                }
            }
            Iterator savedImgs = tmp.iterator();
            while (savedImgs.hasNext()) {
                Object key = savedImgs.next();
                Image saved = (Image) savedImgs.next();
                memoryCache.put(key, saved);
            }
        }

        @Override
        public void mouseMoved(MouseEvent event) {
            Point point = event.getPoint();
            int index = list.locationToIndex(point);
            //Get the value of the item in the list
            Rectangle cell = null;
            //this range should always be inside the frame, so not null.
            boolean isInside = index >= 0 && (cell = list.getCellBounds(index, index)).contains(point);
            if (isInside) {
                ListModel model = list.getModel();
                Object value;
                if (index != mouseIndex) {
                    list.repaint(cell);
                    Rectangle oldRect = list.getCellBounds(mouseIndex, mouseIndex);
                    //still reachable
                    if (oldRect != null) {
                        list.repaint(oldRect);
                    }
                    mouseValue = model.getElementAt(index);
                    mouseIndex = index;
                } else if (mouseValue != (value = model.getElementAt(index))) {
                    list.repaint(cell);
                    mouseValue = value;
                }
            }

        }
    }

    /**
     * A viewport layout that tries to horizontally center a component in
     * the viewport.
     * @author i30817
     */
    private static final class CenteredViewPortLayout implements LayoutManager {

        @Override
        public void addLayoutComponent(String name, Component comp) {
            /*nop*/
        }

        @Override
        public void removeLayoutComponent(Component comp) {
            /*nop*/
        }

        public Dimension maximumLayoutSize(Container target) {
            if (target == null) {
                return new Dimension(0, 0);
            }
            return target.getPreferredSize();
        }

        @Override
        public Dimension preferredLayoutSize(Container parent) {
            Component view = ((JViewport) parent).getView();
            if (view == null) {
                return new Dimension(0, 0);
            } else if (view instanceof Scrollable) {
                return ((Scrollable) view).getPreferredScrollableViewportSize();
            } else {
                return view.getPreferredSize();
            }
        }

        @Override
        public Dimension minimumLayoutSize(Container parent) {
            return new Dimension(4, 4);
        }

        @Override
        public void layoutContainer(Container parent) {
            JViewport port = (JViewport) parent;
            JScrollPane pane = (JScrollPane) port.getParent();
            Component view = port.getView();

            Dimension maximumViewSize = pane.getSize();
            maximumViewSize.width -= pane.getVerticalScrollBar().getWidth();
            Dimension newViewSize = getCellRowSize(view, maximumViewSize);
            int justifiedStartX = (maximumViewSize.width - newViewSize.width) / 2;
            /**_________     __________
             * ||YYY|XX|  -> |X|YYYY|X|
             * ||YYY|XX|     |X|YYYY|X|
             */
//            pane.setBackground(Color.CYAN);
//            port.setBackground(Color.MAGENTA);
//            view.setBackground(Color.ORANGE);
            port.setBounds(justifiedStartX, port.getY(), newViewSize.width, port.getHeight());
            port.setViewSize(newViewSize);
        }

        private static Dimension getCellRowSize(Component view, Dimension maximumViewSize) {
            /**
             * All of the dimensions below are in view coordinates.
             */
            Dimension newViewSize = view.getPreferredSize();

            /**
             * If a JList the prefered size is the prefered size of the
             * sum of cells fitting in a row.
             * Also, the limit is the number of cells.
             */
            if (view instanceof JList) {
                int fixedCellWidth = ((JList) view).getFixedCellWidth();
                int expandLimit = ((JList) view).getModel().getSize();
                if (fixedCellWidth != -1) {
                    newViewSize.width = fixedCellWidth;
                }
                /**
                 * Multiply the cell width until it matches the
                 * n * viewPreferredSize.width + z = viewPort.width
                 * for a z < preferredSize and n <= expandLimit
                 */
                int counter = maximumViewSize.width / newViewSize.width;
                if (counter > expandLimit) {
                    counter = expandLimit;
                }
                newViewSize.width = counter * newViewSize.width;
            }
            //fill to maximum size if it doesn't fit or nothing there
            if (newViewSize.width == 0) {
                newViewSize.width = maximumViewSize.width;
            }
            return newViewSize;
        }
    }
}
